/*******************************************************************************
** 文件名称：gm_calendar.c
** 文件作用：通用日历操作
** 创建作者：Tom Free 付瑞彪
** 创建时间：2018-11-10
**
**          Copyright (c) 2018-2021 付瑞彪 All Rights Reserved
**
**       1 Tab == 4 Spaces     UTF-8     ANSI C Language(C99)
*******************************************************************************/

#include "gm_calendar.h"
#include "stdio.h"

/* 默认时区为北京时区，UTC+8 */
#ifndef GM_CALENDAR_CFG_DEFAULT_TIMEZONE
#define GM_CALENDAR_CFG_DEFAULT_TIMEZONE    8
#elif ((GM_CALENDAR_CFG_DEFAULT_TIMEZONE < -12) ||   \
       (GM_CALENDAR_CFG_DEFAULT_TIMEZONE > 12))
#define GM_CALENDAR_CFG_DEFAULT_TIMEZONE    8
#endif

/* 判断闰年 */
#define LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) ||  \
                         ((year) % 400 == 0))

/* 月天数分配，未考虑闰年 */
static const uint8_t days_of_month[12] =
{
    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

/* 当前时区，UTC标识，正标识东时区，负标识西时区，默认北京时间，东八区，UTC+8 */
static int8_t now_time_zone = GM_CALENDAR_CFG_DEFAULT_TIMEZONE;

#ifdef __cplusplus
extern "C" {
#endif

/* 当前年是否是闰年，提供给应用层 */
bool gm_calendar_is_leap_year(uint16_t year)
{
    return LEAP_YEAR(year) ? true : false;
}

/* 设置用于转换时间的时区，采用UTC标识，东+西-，入北京时间，东八区，UTC+8，输入8 */
bool gm_calendar_set_time_zone(int8_t timezone)
{
    if ((now_time_zone < -12) || (timezone > 12))
    {
        /* 不能超过24小时时区跨度，最大正负12 */
        return false;
    }
    
    now_time_zone = timezone;

    return true;
}

/* 获得当前月份的总天数 */
uint8_t gm_calendar_get_month_days(uint16_t year, uint8_t month)
{
    if ((year == 0) || (month == 0) || (month > 12))
    {
        /* 年与月份错误 */
        return 0;
    }
    
    if ((LEAP_YEAR(year)) && (month == 2))
    {
        /* 闰年二月29天 */
        return 29;
    }
    else
    {
        /* 其它直接返回表对应值 */
        return days_of_month[month - 1];
    }
}

/* 根据日期计算星期 */
uint8_t gm_calandar_calc_week_day(uint16_t year, uint8_t month, uint8_t day)
{
    uint8_t week_day;

    if ((year == 0) ||
        (month == 0) ||
        (month > 12) ||
        (day == 0) ||
        (day > gm_calendar_get_month_days(year, month)))
    {
        /* 年、月份和日期错误 */
        return 0;
    }

    /* 将1/2月转换为上一年的13/14月 */
    if ((month == 1) || 
        (month == 2)) 
    {
        month += 12;
        year--;
    }

    /* 计算星期，采用基姆拉尔森计算公式 */
    week_day = (day +
                2 * month +
                3 * (month + 1) / 5 +
                year +
                year / 4 -
                year / 100 +
                year / 400) % 7 + 1;

    return week_day;
}

/* 计算某日期是那一年的第几天 */
uint16_t gm_calendar_calc_now_days(uint16_t year, uint8_t month, uint8_t day)
{
    uint16_t days = 0;

    if ((year  == 0) ||
        (month == 0) ||
        (month > 12) ||
        (day == 0) ||
        (day > gm_calendar_get_month_days(year, month)))
    {
        /* 年、月份和日期错误 */
        return 0;
    }

    /* 计算到当前月之前的总天数 */
    for (uint8_t i = 1; i < month; i++)
    {
        /* 按月累计 */
        days += days_of_month[i - 1];
    }

    /* 修正闰年 */
    if (LEAP_YEAR(year) && (month > 2))
    {
        /* 闰年二月多一天，所以之后的月都要加1 */
        days++;
    }

    /* 加入当前月的天数 */
    days += day;

    return days;
}

/* 计算某日期从1970年1月1日以来的总天数 */
uint32_t gm_calendar_calc_all_days(uint16_t year, uint8_t month, uint8_t day)
{
    uint32_t days = 0;

    if ((year < 1970) ||
        (month == 0) ||
        (month > 12) ||
        (day == 0) ||
        (day > gm_calendar_get_month_days(year, month)))
    {
        /* 年、月份和日期错误 */
        return 0;
    }

    /* 计算到当前年为止总共天数 */
    for (uint16_t i = 1970; i < year; i++)
    {
        if (LEAP_YEAR(i))
        {
            days += 366ul;
        }
        else
        {
            days += 365ul;
        }
    }

    /* 加上当年的天数 */
    days += gm_calendar_calc_now_days(year, month, day);

    /* 因为上面加的是当年的第几天，包括了1月1日，所以要减去这一天 */
    return (days - 1);
}

/* 通过1970年1月1日以来的总天数计算公历日期 */
void gm_calendar_all_days_to_solar(gm_solar_t *p_solar, uint16_t days)
{
    uint16_t now_year_days;
    uint8_t now_month_days;

    /* 计算年 */
    for (p_solar->year = 1970; ; p_solar->year++)
    {
        now_year_days = gm_calendar_is_leap_year(p_solar->year) ? 366 : 365;
        if (days < now_year_days)
        {
            break;
        }
        days -= now_year_days;
    }

    /* 计算月 */
    for (p_solar->month = 1; ; p_solar->month++)
    {
        now_month_days = gm_calendar_get_month_days(p_solar->year, p_solar->month);
        if (days < now_month_days)
        {
            break;
        }
        days -= now_month_days;
    }

    /* 计算日，因为计算的时候计算的是差值，所以需要加上1号的偏移 */
    p_solar->day = days + 1;
}

/* 判断时间合法性 */
bool gm_calendar_check_time_validity(gm_time_t* p_time)
{
    if ( (p_time->year == 0)  ||
         (p_time->month == 0) || (p_time->month > 12) ||
         (p_time->day == 0)   ||
         (p_time->day > gm_calendar_get_month_days(p_time->year, p_time->month)) ||
         (p_time->hour > 23)  ||
         (p_time->minute > 59)||
         (p_time->second > 59))
    {
        return false;
    }
    else
    {
        return true;
    }
}

/* 时间转换成时间戳，此函数需要时区参与，请保证时区设置OK */
bool gm_calendar_time_to_stamp(gm_time_t* p_time, gm_stamp_t *p_stamp)
{
    uint16_t i;

    if ((p_time == NULL) || (p_stamp == NULL))
    {
        return false;
    }

    /* 初始化为0 */
    *p_stamp = 0;

    if ((p_time->year < 1970) ||
        (gm_calendar_check_time_validity(p_time) != true))
    {
        /* 时间不合法 */
        return false;
    }

    /* 计算1970到去年的总的年秒数 */
    for (i = 1970; i < p_time->year; i++)
    {
        if (LEAP_YEAR(i))
        {
            /* 闰年多一天 */
            *p_stamp += 31622400ul;
        }
        else
        {
            *p_stamp += 31536000ul;
        }
    }

    /* 计算到上个月的秒数 */
    for (i = 1; i < p_time->month; i++)
    {
        *p_stamp += (gm_stamp_t)(days_of_month[i - 1] * 86400ul);
        /* 二月需要多加一天 */
        if ((i == 2) && LEAP_YEAR(p_time->year))
        {
            *p_stamp += 86400ul;
        }
    }

    /* 把前面日期的秒钟数相加 */
    *p_stamp += (gm_stamp_t)((p_time ->day - 1) * 86400uL);
    /* 把前面小时秒钟数相加 */
    *p_stamp += (gm_stamp_t)(p_time->hour * 3600uL);
    /* 把前面分钟秒钟数相加 */
    *p_stamp += (gm_stamp_t)(p_time->minute * 60uL);
    /* 把最后的秒钟加上去 */
    *p_stamp += (gm_stamp_t)(p_time->second);

    /* 减去误差时间(UTC+X) */
    *p_stamp -= (gm_stamp_t)(3600l * now_time_zone);

    return true;
}

/* 时间戳转换为时间，此函数需要时区参与，请保证时区设置OK */
bool gm_calendar_stamp_to_time(gm_stamp_t *p_stamp, gm_time_t* p_time)
{
    uint32_t   total_days;
    uint16_t   year;
    uint8_t    month;
    gm_stamp_t stamp;
    
    if ((p_time == NULL) || (p_stamp == NULL))
    {
        return false;
    }

    stamp = *p_stamp;

    /* 先将时间戳调整到当前时区 */
    stamp += (gm_stamp_t)(3600l * now_time_zone);

    /* 计算出总天数 */
    total_days = stamp / 86400ul;

    /* 从1970开始计算 */
    year = 1970ul;

    /* 判断有无超过一年，直到不够一年为止 */
    while (total_days >= 365ul)
    {
        /* 是闰年 */
        if (LEAP_YEAR(year))
        {
            /* 超过了闰年的天数 */
            if (total_days >= 366ul)
            {
                total_days -= 366ul;
            }
            else
            {
                break;
            }
        }
        else
        {
            /* 平年 */
            total_days -= 365ul;
        }
        year++;
    }

    /* 计算当前年 */
    p_time->year = year;

    month = 1;
    //超过了一个月
    while (total_days >= 28)
    {
        if (LEAP_YEAR(year) && (month == 2))//当年是不是闰年/2月份
        {
            if (total_days >= 29)
            {
                total_days -= 29ul;//闰年的秒钟数
            }
            else
            {
                break;
            }
        }
        else
        {
            if (total_days >= days_of_month[month - 1])
            {
                total_days -= days_of_month[month - 1];//平年
            }
            else
            {
                break;
            }
        }
        month++;
    }

    p_time->month = month;
    p_time->day = total_days + 1;

    total_days = stamp % 86400;  //得到秒钟数
    p_time->hour = total_days / 3600;             //小时
    p_time->minute = (total_days % 3600) / 60;     //分钟
    p_time->second = (total_days % 3600) % 60;     //秒钟
    p_time->week = gm_calandar_calc_week_day(p_time->year, p_time->month, p_time->day);

    return true;
}

/* 计算当前公历的后一天 */
bool gm_calendar_next_day(gm_solar_t *p_solar)
{
    uint8_t cur_month_day;

    if ((p_solar->year == 0) ||
        (p_solar->month == 0) ||
        (p_solar->month > 12) ||
        (p_solar->day == 0))
    {
        return false;
    }

    cur_month_day = gm_calendar_get_month_days(p_solar->year, p_solar->month);
    if (p_solar->day > cur_month_day)
    {
        return false;
    }

    if (p_solar->day >= cur_month_day)
    {
        p_solar->day = 1;
        if (p_solar->month >= 12)
        {
            p_solar->month = 1;
            p_solar->year++;
        }
        else
        {
            p_solar->month++;
        }
    }
    else
    {
        p_solar->day++;
    }

    return true;
}

/* 计算当前公历的前一天 */
bool gm_calendar_prev_day(gm_solar_t *p_solar)
{
    uint8_t cur_month_day;

    if ((p_solar->year == 0) ||
        (p_solar->month == 0) ||
        (p_solar->month > 12) ||
        (p_solar->day == 0))
    {
        return false;
    }

    cur_month_day = gm_calendar_get_month_days(p_solar->year, p_solar->month);
    if (p_solar->day > cur_month_day)
    {
        return false;
    }

    if (p_solar->day == 1)
    {
        if (p_solar->month == 1)
        {
            p_solar->month = 12;
            p_solar->year--;
        }
        else
        {
            p_solar->month--;
        }
        p_solar->day = gm_calendar_get_month_days(p_solar->year, p_solar->month);
    }
    else
    {
        p_solar->day--;
    }

    return true;
}

#ifdef __cplusplus
}
#endif
